package com.photoeditor.demo.util.hook;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

/**
 * Created by @author winn on 18/7/24.
 */

public class ReflectUtils {

    private static final HashMap<String, Field> fieldCache = new HashMap<>();
    private static final HashMap<String, Method> methodCache = new HashMap<>();

    public static Field findField(Class<?> clazz, String fieldName) throws Throwable {
        String fullFieldName = clazz.getName() + '#' + fieldName;

        if (fieldCache.containsKey(fullFieldName)) {
            Field field = fieldCache.get(fullFieldName);
            if (field == null) {
                throw new NoSuchFieldError(fullFieldName);
            }
            return field;
        }

        try {
            Field field = findFieldRecursiveImpl(clazz, fieldName);
            field.setAccessible(true);
            fieldCache.put(fullFieldName, field);
            return field;
        } catch (NoSuchFieldException e) {
            fieldCache.put(fullFieldName, null);
            throw new NoSuchFieldError(fullFieldName);
        }
    }


    private static Field findFieldRecursiveImpl(Class<?> clazz, String fieldName) throws NoSuchFieldException {
        try {
            return clazz.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            while (true) {
                clazz = clazz.getSuperclass();
                if (clazz == null || clazz.equals(Object.class)) {
                    break;
                }
                try {
                    return clazz.getDeclaredField(fieldName);
                } catch (NoSuchFieldException ignored) {
                }
            }
            throw e;
        }
    }


    public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) throws Throwable {
        String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";

        if (methodCache.containsKey(fullMethodName)) {
            Method method = methodCache.get(fullMethodName);
            if (method == null) {
                throw new NoSuchMethodError(fullMethodName);
            }
            return method;
        }

        try {
            Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
            method.setAccessible(true);
            methodCache.put(fullMethodName, method);
            return method;
        } catch (NoSuchMethodException e) {
            methodCache.put(fullMethodName, null);
            throw new NoSuchMethodError(fullMethodName);
        }
    }


    /**
     * Returns an array of the given classes.
     */
    public static Class<?>[] getClassesAsArray(Class<?>... clazzes) {
        return clazzes;
    }

    private static String getParametersString(Class<?>... clazzes) {
        StringBuilder sb = new StringBuilder("(");
        boolean first = true;
        for (Class<?> clazz : clazzes) {
            if (first) {
                first = false;
            } else {
                sb.append(",");
            }

            if (clazz != null) {
                sb.append(clazz.getCanonicalName());
            } else {
                sb.append("null");
            }
        }
        sb.append(")");
        return sb.toString();
    }

    /**
     * Retrieve classes from an array, where each element might either be a Class
     * already, or a String with the full class name.
     */
    private static Class<?>[] getParameterClasses(ClassLoader classLoader, Object[] parameterTypes) throws ClassNotFoundException {
        Class<?>[] parameterClasses = null;
        for (int i = parameterTypes.length - 1; i >= 0; i--) {
            Object type = parameterTypes[i];
            if (type == null) {
                throw new ClassNotFoundException("parameter type must not be null", null);
            }
            if (parameterClasses == null) {
                parameterClasses = new Class<?>[i + 1];
            }

            if (type instanceof Class) {
                parameterClasses[i] = (Class<?>) type;
            } else if (type instanceof String) {
                parameterClasses[i] = findClass((String) type, classLoader);
            } else {
                throw new ClassNotFoundException("parameter type must either be specified as Class or String", null);
            }
        }

        // if there are no arguments for the method
        if (parameterClasses == null) {
            parameterClasses = new Class<?>[0];
        }

        return parameterClasses;
    }

    public static Class<?> findClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
        if (classLoader == null) {
            classLoader = ClassLoader.getSystemClassLoader();
        }
        return classLoader.loadClass(className);
    }


    public static ReflectObject wrap(Object object) {
        return new ReflectObject(object);
    }


    public static class ReflectObject {

        private Object object;

        private ReflectObject(Object o) {
            this.object = o;
        }

        public ReflectObject getChildField(String fieldName) throws Throwable {
            Object child = ReflectUtils.findField(object.getClass(), fieldName).get(object);
            return ReflectUtils.wrap(child);
        }

        public void setChildField(String fieldName, Object o) throws Throwable {
            ReflectUtils.findField(object.getClass(), fieldName).set(object, o);
        }

        public ReflectObject callMethod(String methodName, Object... args) throws Throwable {
            Class<?>[] clazzs = new Class[args.length];
            for (int i = 0; i < args.length; i++) {
                clazzs[i] = args.getClass();
            }
            Method method = ReflectUtils.findMethodExact(object.getClass(), methodName, clazzs);
            return ReflectUtils.wrap(method.invoke(object, args));
        }

        public <T> T getAs(Class<T> clazz) {
            return (T) object;
        }

        public <T> T get() {
            return (T) object;
        }
    }
}
